﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace puzzle
{
    public class SlideField
    {
        public int Width { get; set; }
        public int Height { get; set; }
        private SlideType[,] tiles;
        private bool[,] blockPool;
        private bool[,] normalPool;

        private int warp1X;
        private int warp1Y;
        private int warp2X;
        private int warp2Y;

        private Random random;
        private static int counterBlockTileChance = 4;
        private static int falseBlockTileChance = 4;
        private static int loseTileChance = 5;
        private static int NormalTileChance = 5;
        private static int falseNormalTileChance = 5;
        private static double complexityFactor = 0.75;

        public SlideField(int width, int height)
        {
            Width = width;
            Height = height;
            tiles = new SlideType[width, height];
            blockPool = new bool[width, height];
            normalPool = new bool[width, height];
            random = new Random();

            

            int nbSlide;
            do
            {
                for (int i = 0; i < width; i++)
                {
                    for (int j = 0; j < height; j++)
                    {
                        if (isBorder(i, j, 0))
                        {
                            tiles[i, j] = SlideType.Lose;
                            normalPool[i, j] = false;
                        }
                        else
                        {
                            tiles[i, j] = SlideType.Slide;
                            normalPool[i, j] = true;
                        }
                        blockPool[i, j] = true;
                    }
                }

                

                int startX = random.Next(3, Width - 3);
                int startY = random.Next(3, Height - 3);
                Dir currentDir = randomDir();
                setEndTile(startX, startY, currentDir);

                setWarpers();

                
                int currentX = startX;
                int currentY = startY;
                int deltaX, deltaY, length;
                nbSlide = 0;
                bool canExit = true;

                while (!canExit || getComplexity() < Width * Height * complexityFactor)
                {
                    length = chooseSlideLength(currentX, currentY, currentDir);

                    addSlideTo(ref currentX, ref currentY, currentDir, length);

                    currentDir = chooseDirection(currentX, currentY, currentDir);

                    canExit = addSlideEnd(currentX, currentY, oppositeDir(currentDir));

                    nbSlide++;
                    if (nbSlide > Width * Height / 4) break;
                }

                addSlideTo(ref currentX, ref currentY, currentDir, 1 + getMaxSlideLength(currentX, currentY, currentDir));

                setDeltas(currentDir, out deltaX, out deltaY);
                tiles[currentX, currentY] = SlideType.Slide;
                blockPool[currentX, currentY] = false;
            } while (nbSlide > Width * Height / 4);


            //add random tiles
            for (int i = 0; i < width; i++)
            {
                for (int j = 0; j < height; j++)
                {
                    if (normalPool[i, j] && random.Next(loseTileChance) == 0) tiles[i, j] = SlideType.Lose;
                    else if (normalPool[i, j] && random.Next(falseNormalTileChance) == 0) tiles[i, j] = SlideType.Normal;
                    else if (blockPool[i, j] && random.Next(falseBlockTileChance) == 0) tiles[i, j] = SlideType.Block;
                }
            }

        }

        private int chooseSlideLength(int currentX, int currentY, Dir currentDir){
            int deltaX, deltaY;
            List<int> res = new List<int>();
            setDeltas(currentDir, out deltaX, out deltaY);
            int x = currentX + deltaX;
            int y = currentY + deltaY;
            int maxLength = getMaxSlideLength(currentX, currentY, currentDir);
            for (int i = 1; i <= maxLength; i++)
            {
                checkWarp(ref x, ref y);
                if (tiles[x,y] != SlideType.Warp)
                {
                    if (deltaX == 0)
                    {
                        if (blockPool[x + 1, y] || blockPool[x - 1, y] || normalPool[x, y])
                        {
                            res.Add(i);
                        }
                    }
                    else
                    {
                        if (blockPool[x, y + 1] || blockPool[x, y - 1] || normalPool[x, y])
                        {
                            res.Add(i);
                        }
                    }
                }
                x += deltaX;
                y += deltaY;
            }

            if (res.Count == 0) return 0;
            return res[random.Next(res.Count)];
        }

        private Dir chooseDirection(int currentX, int currentY, Dir currentDir){
            Dir res;
            if (currentDir == Dir.Right || currentDir == Dir.Left)
            {
                if (random.Next(2) == 0) res = Dir.Down;
                else res = Dir.Up;
            }
            else
            {
                if (random.Next(2) == 0) res = Dir.Left;
                else res = Dir.Right;
            }

            int deltaX, deltaY;
            setDeltas(res, out deltaX, out deltaY);

            if (chooseSlideLength(currentX, currentY, currentDir) == 0) res = oppositeDir(res);

            return res;
        }

        private void setEndTile(int x, int y, Dir dir)
        {
            tiles[x, y] = SlideType.Normal;
            switch (dir)
            {
                case Dir.Left:
                    tiles[x, y + 1] = SlideType.Block;
                    tiles[x + 1, y + 1] = SlideType.Block;
                    tiles[x + 1, y] = SlideType.Block;
                    tiles[x + 1, y - 1] = SlideType.Block;
                    tiles[x, y - 1] = SlideType.Block;
                    normalPool[x, y + 1] = false;
                    normalPool[x + 1, y + 1] = false;
                    normalPool[x + 1, y] = false;
                    normalPool[x + 1, y - 1] = false;
                    normalPool[x, y - 1] = false;
                    blockPool[x - 1, y - 1] = false;
                    normalPool[x - 1, y] = false;
                    blockPool[x - 1, y + 1] = false;
                    break;
                case Dir.Up:
                    tiles[x + 1, y] = SlideType.Block;
                    tiles[x + 1, y - 1] = SlideType.Block;
                    tiles[x, y - 1] = SlideType.Block;
                    tiles[x - 1, y - 1] = SlideType.Block;
                    tiles[x - 1, y] = SlideType.Block;
                    normalPool[x + 1, y] = false;
                    normalPool[x + 1, y - 1] = false;
                    normalPool[x, y - 1] = false;
                    normalPool[x - 1, y - 1] = false;
                    normalPool[x - 1, y] = false;
                    blockPool[x - 1, y + 1] = false;
                    normalPool[x, y + 1] = false;
                    blockPool[x + 1, y + 1] = false;
                    break;
                case Dir.Right:
                    tiles[x, y - 1] = SlideType.Block;
                    tiles[x - 1, y - 1] = SlideType.Block;
                    tiles[x - 1, y] = SlideType.Block;
                    tiles[x - 1, y + 1] = SlideType.Block;
                    tiles[x, y + 1] = SlideType.Block;
                    normalPool[x, y - 1] = false;
                    normalPool[x - 1, y - 1] = false;
                    normalPool[x - 1, y] = false;
                    normalPool[x - 1, y + 1] = false;
                    normalPool[x, y + 1] = false;
                    blockPool[x + 1, y + 1] = false;
                    normalPool[x + 1, y] = false;
                    blockPool[x + 1, y - 1] = false;
                    break;
                case Dir.Down:
                    tiles[x - 1, y] = SlideType.Block;
                    tiles[x - 1, y + 1] = SlideType.Block;
                    tiles[x, y + 1] = SlideType.Block;
                    tiles[x + 1, y + 1] = SlideType.Block;
                    tiles[x + 1, y] = SlideType.Block;
                    normalPool[x - 1, y] = false;
                    normalPool[x - 1, y + 1] = false;
                    normalPool[x, y + 1] = false;
                    normalPool[x + 1, y + 1] = false;
                    normalPool[x + 1, y] = false;
                    blockPool[x + 1, y - 1] = false;
                    normalPool[x, y - 1] = false;
                    blockPool[x - 1, y - 1] = false;
                    break;
            }
        }

        private int getMaxSlideLength(int xEnd, int yEnd, Dir dir){
            int deltaX, deltaY;
            setDeltas(dir, out deltaX, out deltaY);
            int length = 0;
            int x = xEnd + deltaX;
            int y = yEnd + deltaY;
            while (!isBorder(x, y, 0) && tiles[x, y] != SlideType.Block && tiles[x + deltaX, y + deltaY] != SlideType.Normal)
            {
                checkWarp(ref x, ref y);

                length++;
                x += deltaX;
                y += deltaY;
            }

            return length;
        }

        private bool addSlideEnd(int xEnd, int yEnd, Dir dir)
        {
            int deltaX, deltaY;
            setDeltas(dir, out deltaX, out deltaY);

            if (blockPool[xEnd + deltaX, yEnd + deltaY] && !(normalPool[xEnd, yEnd] && random.Next(NormalTileChance) == 0))
            {
                tiles[xEnd + deltaX, yEnd + deltaY] = SlideType.Block;
                normalPool[xEnd + deltaX, yEnd + deltaY] = false;
            }
            else if (normalPool[xEnd, yEnd])
            {
                tiles[xEnd, yEnd] = SlideType.Normal;
                int x = xEnd + deltaX;
                int y = yEnd + deltaY;
                int maxLength = getMaxSlideLength(xEnd, yEnd, dir);
                for (int i = 1; i <= maxLength; i++)
                {
                    checkWarp(ref x, ref y);

                    normalPool[x, y] = false;
                    if (deltaX == 0)
                    {
                        blockPool[x + 1, y] = false;
                        blockPool[x - 1, y] = false;
                    }
                    else
                    {
                        blockPool[x, y + 1] = false;
                        blockPool[x, y - 1] = false;
                    }
                    x += deltaX;
                    y += deltaY;
                }
            }
            blockPool[xEnd, yEnd] = false;
            normalPool[xEnd, yEnd] = false;

            bool canExit = false; //todo bug warp to exit?
            int x2 = xEnd;
            int y2 = yEnd;
            int maxLength2 = getMaxSlideLength(xEnd, yEnd, oppositeDir(dir));
            for (int i = 0; i < maxLength2; i++)
            {
                checkWarp(ref x2, ref y2);

                if (blockPool[x2, y2] || normalPool[x2, y2]) canExit = true;
                x2 -= deltaX;
                y2 -= deltaY;
            }
            return canExit && isBorder(x2 - deltaX, y2 - deltaY, 0);
        }

        private void setDeltas(Dir dir, out int deltaX, out int deltaY)
        {
            deltaX = 0;
            deltaY = 0;
            switch (dir)
            {
                case Dir.Left: deltaX = -1; deltaY = 0; break;
                case Dir.Up: deltaX = 0; deltaY = 1; break;
                case Dir.Right: deltaX = 1; deltaY = 0; break;
                case Dir.Down: deltaX = 0; deltaY = -1; break;
            }
        }

        private void addSlideTo(ref int xEnd, ref int yEnd, Dir dir, int length){
            int deltaX, deltaY;
            setDeltas(dir, out deltaX, out deltaY);

            bool falseBlock = false;
            int x = xEnd;
            int y = yEnd;
            int maxLength = Math.Max(length, getMaxSlideLength(xEnd, yEnd, dir));
            for (int i = 0; i <= maxLength; i++)
            {
                checkWarp(ref x, ref y);

                if (i < length)
                {
                    normalPool[x, y] = false;
                    blockPool[x, y] = false;
                    if (deltaX == 0)
                    {
                        blockPool[x + 1, y] = false;
                        blockPool[x - 1, y] = false;
                    }
                    else
                    {
                        blockPool[x, y + 1] = false;
                        blockPool[x, y - 1] = false;
                    }
                }

                if (i == length)
                {
                    xEnd = x;
                    yEnd = y;
                }

                if (i > length && !falseBlock)
                {
                    normalPool[x, y] = false;

                    if (blockPool[x,y] && random.Next(counterBlockTileChance) == 0)
                    {
                        falseBlock = true; 
                        tiles[x, y] = SlideType.Block;
                        
                    }
                    else
                    {
                        if (deltaX == 0)
                        {
                            blockPool[x + 1, y] = false;
                            blockPool[x - 1, y] = false;
                        }
                        else
                        {
                            blockPool[x, y + 1] = false;
                            blockPool[x, y - 1] = false;
                        }
                    }
                }

                x += deltaX;
                y += deltaY;
            }
        }


        private void setWarpers(){
            do {
                warp1X = random.Next(1, Width - 1);
                warp1Y = random.Next(1, Height - 1);
                warp2X = random.Next(1, Width - 1);
                warp2Y = random.Next(1, Height - 1);
            } while (tiles[warp1X, warp1Y] != SlideType.Slide || tiles[warp2X, warp2Y] != SlideType.Slide || warp1X == warp2X || (warp1Y == warp2Y));

            tiles[warp1X, warp1Y] = SlideType.Warp;
            tiles[warp2X, warp2Y] = SlideType.Warp;
            normalPool[warp1X, warp1Y] = false;
            normalPool[warp2X, warp2Y] = false;
            blockPool[warp1X, warp1Y] = false;
            blockPool[warp2X, warp2Y] = false;
        }

        private bool checkWarp(ref int x, ref int y)
        {
            if (x == warp1X && y == warp1Y)
            {
                x = warp2X;
                y = warp2Y;
                return true;
            }
            else if (x == warp2X && y == warp2Y)
            {
                x = warp1X;
                y = warp1Y;
                return true;
            }

            return false;
        }

        public int getComplexity()
        {
            int c = 0;
            for (int i = 0; i < Width; i++)
            {
                for (int j = 0; j < Height; j++)
                {
                    if (!blockPool[i, j]) c++;
                }
            }

            return c;
        }

        public SlideType getTile(int x, int y)
        {
            return tiles[x, y];
        }

        public bool getBlockPool(int x, int y)
        {
            return blockPool[x, y];
        }

        public bool getNormalPool(int x, int y)
        {
            return normalPool[x, y];
        }

        private bool isBorder(int x, int y, int dist){
            return (x == 0 + dist || y == 0 + dist || x == Width - dist - 1 || y == Height - dist - 1);
        }

        private Dir oppositeDir(Dir dir)
        {
            switch (dir)
            {
                case Dir.Up: return Dir.Down;
                case Dir.Down: return Dir.Up;
                case Dir.Right: return Dir.Left;
                case Dir.Left: return Dir.Right;
                default: return Dir.Down;
            }
        }

        private Dir randomDir(){
            int r = random.Next(4);
            switch(r)
            {
                case 0: return Dir.Down;
                case 1: return Dir.Up;
                case 2: return Dir.Left;
                case 3: return Dir.Right;
            }
            return Dir.Down;
        }
    }

    

    public enum SlideType
    {
        Slide,
        Normal,
        Block,
        Lose,
        Warp
    }

    public enum Dir
    {
        Right,
        Down,
        Left,
        Up
    }

}
